package parser;

import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Scanner;
import java.util.StringTokenizer;

/**
 * @author Artur Tolstenco
 * 
 */

/**
 * A class that implements the parser.
 * The grammar is: 
 * 		File ::= Row | Row File
 * 		Row ::= Name : Arcs
 * 		Arcs ::= Name -> Weight | Name -> Weight; Arcs
 * 
 * The Name is a sequence of lower or upper case letters (thus not containing numbers, 
 * spaces, etc. etc.)
 * There can be any number of spaces.
 */
public class Parser {
	private File file;
	private ArrayList<HashMap<String, ArrayList<HashMap<String, Double>>>> result;

	private int rowLine = 0;

	/**
	 * Constructor that takes she filename as a parameter
	 */
	public Parser(File file) {
		super();
		this.file = file;
	}

	/***
	 * 
	 * @return in memory representation of the file parsed
	 */
	public ArrayList<HashMap<String, ArrayList<HashMap<String, Double>>>> getResult() {
		return result;
	}

	/**
	 * Function that parses the File file given to the constructor.
	 * 
	 * Reads the file line by line as specified by the grammar: 
	 * File ::= Row | Row File
	 * 
	 * Note: the blank file is not a well-formed file
	 * 
	 * @return true if the file is well-formed, false otherwise
	 * @throws FileNotFoundException if the File file given to 
	 * 									constructor does not exists
	 */
	public final boolean process() throws FileNotFoundException {
		Scanner scanner = new Scanner(new FileReader(file));
		result = new ArrayList<HashMap<String, ArrayList<HashMap<String, Double>>>>();
		boolean parseResult = false;

		try {
			while (scanner.hasNextLine()) {
				++rowLine;
				if (!(parseResult = processRow(scanner.nextLine())))
					break;
			}
		} finally {
			if (result.size()==0) parseResult = false;
			scanner.close();
		}

		if (!parseResult)
			result = null;

		return parseResult;
	}

	/**
	 * The method that takes in input the String lineToPars and parses it as
	 * specified by the grammar: Row ::= Name : Arcs
	 * 
	 * Note: Name is a sequence of upper or lower case letters! No spaces, numbers, 
	 * etc. allowed!
	 * 
	 * @param lineToParse
	 * @return true if the line is well-formed (according to the grammar), false
	 *         otherwise
	 */
	private boolean processRow(String lineToParse) {
		StringTokenizer st = new StringTokenizer(lineToParse.trim(), ":", true);

		if (st.countTokens()==0) return true; //riga vuota
		
		if (st.hasMoreTokens()) {
			String name = st.nextToken().trim();
			
			if (!name.matches("[a-zA-Z]+")) {
				log("Name (at the begining) must be a sequence of upper case or lower case letters line number " + rowLine);
				return false;
			}
			
			HashMap<String, ArrayList<HashMap<String, Double>>> row = new HashMap<String, ArrayList<HashMap<String, Double>>>();

			// lista di adiacenza vuota
			ArrayList<HashMap<String, Double>> lista_di_adiacenza = new ArrayList<HashMap<String, Double>>();

			row.put(name, lista_di_adiacenza);
			result.add(row);
		} else {
			log("Expecting Name (at the beginning) on the line number "	+ rowLine);
			return false;
		}

		if (!st.hasMoreTokens() || !st.nextToken().trim().equals(":")) {
			log("Expecting ':' after the Name on the line number " + rowLine);
			return false;
		}

		if (st.hasMoreTokens()) {
			String arcs = st.nextToken().trim();

			if (arcs.endsWith(";")) {
				log("Invalid end of line number " + rowLine);
				return false;
			}

			boolean toreturn = processArcs(arcs);

			if (st.hasMoreTokens() || !toreturn)
				return false;

			return true;

		}

		log("Expecting Arcs on the line number " + rowLine);
		return false;

	}

	/***
	 * The method that takes in input a String passed from ProcessRow(string)
	 * and analyzes the Arcs (Row ::= Name : Arcs): 
	 * Arcs ::= Name -> Weight | Name -> Weight; Arcs
	 * 
	 * Note: there must be at least ONE arc (edge)!
	 * 
	 * @param arcs
	 * @return true if the string passed as parameter is well-formed string 
	 * 			containing arcs (according to the grammar), false otherwise
	 */
	private boolean processArcs(String arcs) {
		Scanner scannerForArcs = new Scanner(arcs).useDelimiter(";");

		if (scannerForArcs.hasNext()) {
			if (processArc(scannerForArcs.next())) {
				while (scannerForArcs.hasNext()) {
					if (!processArc(scannerForArcs.next()))
						return false;
				}
				return true;
			} else {
				log("Error parsing the first Arc on the line number " + rowLine);
				return false;
			}
		}

		log("Expecting an Arc (minimum one) on the line number " + rowLine);
		return false;
	}

	/***
	 * The method that takes in input a String and analyzes if it is a well
	 * formed string representing an arc (Arc ::= Name -> Weight)
	 * 
	 * Note: the name is a sequence of lower or upper case letters!
	 * 
	 * @param arc
	 * @return true if the string represents an arc (according to the grammar)
	 */
	private boolean processArc(String arc) {
		Scanner scannerForArc = new Scanner(arc).useDelimiter("-> *");
		String arcName;
		Double arcWeight;

		if (scannerForArc.hasNext()) {
			arcName = scannerForArc.next().trim();
			if (!arcName.matches("[a-zA-Z]+")) {
				log("Name of the Arc must be a sequence of upper case or lower case letters line number " + rowLine);
				return false;
			}
		} else {
			log("Expecting Name of the Arc on the line number " + rowLine);
			return false;
		}

		if (scannerForArc.hasNextDouble())
			arcWeight = scannerForArc.nextDouble();
		else {
			log("Expecting Weight of the arc on the line number " + rowLine);
			return false;
		}

		if (!scannerForArc.hasNext()) {
			HashMap<String, Double> arcNameWeight = new HashMap<String, Double>();
			arcNameWeight.put(arcName, arcWeight);

			String rowName = result.get(result.size() - 1).keySet()
					.toArray(new String[0])[0];
			result.get(result.size() - 1).get(rowName).add(arcNameWeight);

			return true;
		}

		return false;
	}

	/**
	 * Utility function. It simply prints the error!
	 * @param error
	 */
	private static void log(String error) {
		System.out.println(error);
	}
	

}
